e-Statより「平成22年国勢調査(小地域)2010/10/01」のシェープファイルを入手し(国立以外ならどこでもよい),d3.js + leaflet の組合せで地図上にマッピングしてください.
回答例:
まず,シェープファイルを入手し,GeoJSON形式またはTopoJSON形式に変換します. その上で可視化用のスクリプトを作成します.
script.js(GeoJSON形式の場合):
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
d3.select("#screen").attr("id","map").attr("style", "width: "+width+"px; height: "+height+"px;");
// 地図の描画
var map = L.map('map').setView([35.70089,139.4300], 14);
var credit = '<a href="http://portal.cyberjapan.jp/help/termsofuse.html">国土地理院</a>';
var tileurl = 'http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png';
L.tileLayer(tileurl, {maxZoom: 18, attribution: credit,}).addTo(map);
// GeoJSONの読み込み
d3.json("hino.topojson", function(json) {
// 人口の最大値,最小値,中央値の取得
var max = d3.max(json.features,function(a){return a.properties.JINKO;})
var min = d3.min(json.features,function(a){return a.properties.JINKO;})
var median = d3.median(json.features,function(a){return a.properties.JINKO;})
// 塗り色で人口を表現するための正規化関数の定義
var colorScale = d3.scale.linear()
.domain([min,median,max])
.interpolate(d3.interpolateRgb)
.range(["#FFFFFF", "#FFFF00","#FF0000"]);
// 線の色、塗り色の設定
new L.GeoJSON(json, { style: function(feature) {
return {color: '#000000','fillColor': colorScale(feature.properties.JINKO), 'fillOpacity': 0.5};
},
// popupの登録
onEachFeature: function (feature, layer) {
layer.bindPopup(feature.properties.MOJI+"<br/>人口:"+feature.properties.JINKO);
}}).addTo(map);
});
};
script.js(TpopJSON形式の場合):
GeoJSON形式の場合との違いは下記箇所だけです(TopoJSONもGeoJSON形式に変換してから処理しているため).
d3.json("hino.topojson", function(data) {
var json = topojson.feature(data,data.objects.hino);
コード全体は以下のようになります.
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
d3.select("#screen").attr("id","map").attr("style", "width: "+width+"px; height: "+height+"px;");
// 地図の描画
var map = L.map('map').setView([35.70089,139.4300], 14);
var credit = '<a href="http://portal.cyberjapan.jp/help/termsofuse.html">国土地理院</a>';
var tileurl = 'http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png';
L.tileLayer(tileurl, {maxZoom: 18, attribution: credit,}).addTo(map);
// TopoJSONの読み込み
d3.json("hino.topojson", function(data) {
var json = topojson.feature(data,data.objects.hino);
// 人口の最大値,最小値,中央値の取得
var max = d3.max(json.features,function(a){return a.properties.JINKO;})
var min = d3.min(json.features,function(a){return a.properties.JINKO;})
var median = d3.median(json.features,function(a){return a.properties.JINKO;})
// 塗り色で人口を表現するための正規化関数の定義
var colorScale = d3.scale.linear()
.domain([min,median,max])
.interpolate(d3.interpolateRgb)
.range(["#FFFFFF", "#FFFF00","#FF0000"]);
// 線の色、塗り色の設定
new L.GeoJSON(json, { style: function(feature) {
return {color: '#000000','fillColor': colorScale(feature.properties.JINKO), 'fillOpacity': 0.5};
},
// popupの登録
onEachFeature: function (feature, layer) {
layer.bindPopup(feature.properties.MOJI+"<br/>人口:"+feature.properties.JINKO);
}}).addTo(map);
});
};
e-Statより「平成22年国勢調査(小地域)2010/10/01」のシェープファイルおよび統計データ「年齢別(5歳階級、4区分)、男女別人口」を入手し(国立以外ならどこでもよい),「地域別高齢者(65歳以上)人口比率」がみえるように d3.js + leaflet の組合せで可視化してください.
回答例:
入手したシェープファイルと統計データを合成し,一つのシェープファイルにした後, GeoJSON形式またはTopoJSON形式に変換します.
シェープファイルはRで作成します.
if(!require(maptools)){
install.packages("maptools");
library(maptools);
}
if(!require(dplyr)){
install.packages("dplyr");
library(dplyr);
}
# シェープファイルを読み込む
shp<-maptools::readShapePoly("A002005212010DDSWC13212//h22ka13212.shp")
# shp@data$KEY_CODEを数値にする
shp@data$KEY_CODE<-as.numeric(as.character(shp@data$KEY_CODE))
# CSVファイルを読み込む
csv<-read.csv("tblT000573C13212.txt",stringsAsFactors = FALSE,fileEncoding = "SJIS")
# 不要な1行目(日本語の説明)を削除する
csv <- csv[-1,]
csv$CITYNAME <- iconv(csv$CITYNAME,from="sjis",to="utf8")
csv$NAME <- iconv(csv$NAME,from="sjis",to="utf8")
# shp@dataとcsvをKEY_CODEをキーに結合する
shp@data<-dplyr::inner_join(shp@data,csv,by="KEY_CODE")
# shp@data$T000573019を数値にする(65歳以上人口を数値にする)
shp@data$T000573019 <- as.numeric(as.character(shp@data$T000573019))
# shp@dataにRATE列を追加
shp@data$RATE <- shp@data$T000573019/shp@data$JINKO
# 日本語をUTF8にする
shp@data$MOJI <- iconv(shp@data$MOJI,from="sjis",to="utf8")
shp@data$GST_NAME <- iconv(shp@data$GST_NAME,from="sjis",to="utf8")
shp@data$KEN_NAME <- iconv(shp@data$KEN_NAME,from="sjis",to="utf8")
# 不要なデータを削除する
shp@data$DUMMY1 <- NULL
# シェープファイルを書き出す
maptools::writePolyShape(shp,"tmp/hino")
# 座標系設定
sp::proj4string(shp) <- sp::CRS("+proj=longlat +datum=WGS84");
# ファイル出力
if(!file.exists("hino.json")){
# GeoJSON形式で出力
rgdal::writeOGR(obj = shp,dsn = "hino.json",layer = "hino",driver="GeoJSON",check_exists = FALSE);
}Rまたはウェブサービスを使ってGeoJSONファイルを変換します.
script.js
可視化用のJavaScriptは以下のとおりです.
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
d3.select("#screen").attr("id","map").attr("style", "width: "+width+"px; height: "+height+"px;");
// 地図の描画
var map = L.map('map').setView([35.70089,139.4300], 14);
var credit = '<a href="http://portal.cyberjapan.jp/help/termsofuse.html">国土地理院</a>';
var tileurl = 'http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png';
L.tileLayer(tileurl, {maxZoom: 18, attribution: credit,}).addTo(map);
// GeoJSONの読み込み
d3.json("hino.topojson", function(data) {
var json = topojson.feature(data,data.objects.hino);
// 人口の最大値,最小値,中央値の取得
var max = d3.max(json.features,function(a){return a.properties.RATE;})
var min = d3.min(json.features,function(a){return a.properties.RATE;})
var median = d3.median(json.features,function(a){return a.properties.RATE;})
// 塗り色で人口を表現するための正規化関数の定義
var colorScale = d3.scale.linear()
.domain([min,median,max])
.interpolate(d3.interpolateRgb)
.range(["#FFFFFF", "#FFFF00","#FF0000"]);
// 線の色、塗り色の設定
new L.GeoJSON(json, { style: function(feature) {
return {color: '#000000','fillColor': colorScale(feature.properties.RATE), 'fillOpacity': 0.5};
},
// popupの登録
onEachFeature: function (feature, layer) {
layer.bindPopup(feature.properties.MOJI+"<br/>人口:"+feature.properties.JINKO+"<br/> 65歳以上人口:"+feature.properties.T000573019+"<br/>比率:"+feature.properties.RATE);
}}).addTo(map);
});
};
e-Statより「平成22年国勢調査(小地域)2010/10/01」のシェープファイルおよび統計データ「年齢別(5歳階級、4区分)、男女別人口」を入手し(国立以外ならどこでもよい),「地域別高齢者(65歳以上)人口比率」がみえるように d3.js のみで可視化してください.
回答例:
script.js
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
var svg = d3.select("#screen")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
// GeoJSONの読み込み
d3.json("hino.topojson", function(data) {
var json = topojson.feature(data,data.objects.hino);
console.log(d3.geo.centroid(json));
var projection,path;
// 投影関数定義(メルカトル図法)
projection = d3.geo.mercator()
.scale(450000)
// 中心座標を設定
.center([139.4007152,35.6634249])
.translate([width/2,height/2]);
// 投影方法設定
path = d3.geo.path().projection(projection);
var bounds = path.bounds(json);
console.log(bounds);
// 人口の最大値,最小値,中央値の取得
var max = d3.max(json.features,function(a){return a.properties.RATE;})
var min = d3.min(json.features,function(a){return a.properties.RATE;})
var median = d3.median(json.features,function(a){return a.properties.RATE;})
// 塗り色で人口を表現するための正規化関数の定義
var colorScale = d3.scale.linear()
.domain([min,median,max])
.interpolate(d3.interpolateRgb)
.range(["#FFFFFF", "#FFFF00","#FF0000"]);
// 描画
svg.selectAll('path').data(json.features).enter()
.append('path')
.attr('d', path)
.attr('fill',function(d){return colorScale(d.properties.RATE);})
.attr("stroke",d3.rgb(0,0,0));
});
};
今回は気象庁が公開している台風進路データを可視化します。 また,d3.js の醍醐味であるインタラクティブな可視化にもトライします.
RSMC Tokyo-Typhoon Center: Best Track Data
まずは台風進路データを使って普通に可視化します.
まず、Rを使ってデータを使いやすい形に加工します。
気象庁が公開する台風進路データはテキストデータではあるのですが, これまでのCSV形式と異なり,各フィールが固定長で分割されたデータとなっています。 このままではd3.jsの処理には不向きなので,これをCSV形式に変換します。
Rで固定長のデータを読み込む場合は read.fwf 関数を使います。read.fwf 関数では各フィールドが何桁(何文字数)使用しているかを指定し,ファイルを読み込みます。
以下に読み込み用のコードを示します。 固定長データの読み込みの他,西暦情報の加工や緯度・経度情報の加工も実施しています。
# ファイルがなければインターネットからファイルをダウンロードし,展開する
if(!file.exists("bst_all.zip")){
download.file(url="http://www.jma.go.jp/jma/jma-eng/jma-center/rsmc-hp-pub-eg/Besttracks/bst_all.zip",destfile="bst_all.zip",method = "auto")
unzip("bst_all.zip",exdir = ".")
}
#一行ずつ読み込む
tmp <- readLines("bst_all.txt")
#66666で始まるヘッダー行のみを取り出す
header_line_no <- grep("^66666", tmp)
#Header: 1:Indicator, 2:International number ID, 3:Number of data lines, 4:Tropical cyclone number ID, 5:International number ID, 6:Flag of the last data line, 7:Difference between the time of the last data and the time of the final analysis, 8:Name of the storm, 9:Date of the latest revision
header<-read.fwf(textConnection(tmp[header_line_no]),widths = c(6,5,4,5,5,2,2,21,9),col.names = c("IND","ID","N","TCID","ID2","FLG","DEF","NAME","REV"),fill=TRUE,header = FALSE,colClasses="character")
#entry: 1:Time of analysis, 2:Grade, V3:Latitude of the center, 4:Longitude of the center, 5:Central pressure, 6:Maximum sustained wind speed, 7:Direction of the longest radius of 50kt winds or greater, 8:The longest radius of 50kt winds or greater, 9:The shortest radius of 50kt winds or greater, 10:Direction of the longest radius of 30kt winds or greater, 11:The longest radius of 30kt winds or greater, 12:The shortest radius of 30kt winds or greater, 13:Indicator of landfall or passage
entry <- read.fwf(textConnection(tmp[-header_line_no]),widths=c(9,4,2,4,5,5,4,2,5,5,2,5,5,2),col.names = c("TIME","IND","GRADE","LAT","LON","hPa","MWS","WSD","WSLR","WSSR","SWD","SWLR","SWSR","SIZE","STR","LND"),header = FALSE,fill=TRUE,colClasses="character")
#ヘッダーにIDをつけます
entry$ID<-rep(header$ID,header$N)
#IDをキーにヘッダーとデータをマージします
data<-merge(header,entry,by= "ID")
#観測年(西暦2桁)を西暦4桁に変換します
#substring関数を用いて先頭二文字を取り出します
data$YEAR<-as.numeric(substring(data$TIME,0,2))
#2桁が16より上の場合は1900を足します
data$YEAR[data$YEAR>16]<-data$YEAR[data$YEAR>16]+1900
#16以下の場合は2000を足します
data$YEAR[data$YEAR<=16]<-data$YEAR[data$YEAR<=16]+2000
#日時データとして処理できるようにします
data$TIME[data$YEAR>=2000]<-paste("20",data$TIME[data$YEAR>=2000],sep = "")
data$TIME[data$YEAR<2000]<-paste("19",data$TIME[data$YEAR<2000],sep = "")
# 日付形式に変換します
data$TIME<-strptime(data$TIME, "%Y%m%d%H")
#緯度、軽度を修正します
data$LAT <- as.numeric(data$LAT)/10
data$LON <- as.numeric(data$LON)/10
#台風名に両端の空白を削除します
# trimws はR 3.2.0以降で利用可能
#data$NAME <- trimws(data$NAME,which = "both")
data$ID <- trimws(data$ID,"both")
data$hPa <- as.numeric(data$hPa)
data$GRADE <- trimws(as.character(data$GRADE),which = "both")
data$GRADE <- as.numeric(data$GRADE)
data$NAME <- trimws(data$NAME,"both")
data$UNIXTIME <- as.numeric(as.POSIXct(data$TIME,tz = "Japan"))
# CSV形式でファイルを出力します
write.csv(data,file="typhoon.csv")出力したファイルを開き, CSV形式となっていることを確認してください.
さっそく,加工したデータを d3js を使って可視化してみましょう。
script.js を以下のように変更します.
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// 経度を幅の範囲に正規化
var xScale = d3.scale.linear().domain([-180,180]).range([0,width]);
// 緯度を高さの範囲に正規化
var yScale = d3.scale.linear().domain([-90,90]).range([height,0]);
// 半径データの正規化関数を定義する
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
// 色情報の正規化関数を定義する
var color = d3.scale.category20();
// 台風進路データを読み込む
d3.csv("typhoon.csv",function(error,csv){
// 2015年だけのデータを取り出す(フィルタする)
filtered= csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
// 台風を可視化する
svg.selectAll("circle")
.data(filtered)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return xScale(d.LON);})
.attr("cy",function(d){return yScale(d.LAT);})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
index.html はこれまでと同一で構いません.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ひな形HTMLファイル</title>
<!-- d3.jsライブラリの読み込み -->
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v2.min.js"></script>
<!-- leaflet -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.js"></script>
<!-- スタイルシート -->
<link rel="stylesheet" href="style.css" type="text/css" />
<!-- 処理用スクリプト -->
<script type="text/javascript" src="script.js" ></script>
</head>
<body onload="init();">
<div id="screen"></div>
</body>
</html>
style.css も同じです.
#screen {
// 背景は黒
background: #000000;
}
#svg {
// SVG領域の背景は白
background: #ffffff;
}
出力結果は以下のとおりです.
今回の台風進路データは日本近海を対象としています. 緯度,経度の範囲を狭め,マッピング範囲をもう少し絞ります.
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// 経度を幅の範囲に正規化
var xScale = d3.scale.linear().domain([122,154]).range([0,width]);
// 緯度を高さの範囲に正規化
var yScale = d3.scale.linear().domain([20,46]).range([height,0]);
// 半径データの正規化関数を定義する
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
// 色情報の正規化関数を定義する
var color = d3.scale.category20();
// 台風進路データを読み込む
d3.csv("typhoon.csv",function(error,csv){
// 2015年だけのデータを取り出す(フィルタする)
filtered= csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
// 台風を可視化する
svg.selectAll("circle")
.data(filtered)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return xScale(d.LON);})
.attr("cy",function(d){return yScale(d.LAT);})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
変更後の可視化結果は以下のとおりです. 画面全体に表示されるようになったと思います. 正規化関数はデータの性質に応じてうまく定義してください.
今度は台風進路データとシェープファイルを合わせて可視化します. シェープファイルは前回作成した「JPN_adm0.topojson」を使います.
script.js を以下のように変更します.
/* onloadで呼び出される関数 */
function init(){
var width = window.innerWidth;
var height = window.innerHeight;
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// 座標変換用の関数定義
var prj = d3.geo.mercator()
.center([139.44,35.39])
.translate([width/2, height/2])
.scale(1000);
// マッピング方法の設定
var path = d3.geo.path().projection(prj);
// topoJSONファイルの読み込み
var url = "JPN_adm0.topojson";
d3.json(url, function(error, json) {
// GeoJSON形式に変換
var geojson = topojson.feature(json, json.objects.JPN_adm0);
svg.selectAll('path')
.data(geojson.features).enter()
.append('path')
.attr('d', path)
.attr('fill','rgba(0,0,0,0.8)');
});
// 半径データの正規化関数を定義する
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
// 色情報の正規化関数を定義する
var color = d3.scale.category20();
// 台風進路データを読み込む
d3.csv("typhoon.csv",function(error,csv){
// 2015年だけのデータを取り出す(フィルタする)
filtered= csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
// 台風を可視化する
svg.selectAll("circle")
.data(filtered)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
// 投影関数を使って緯度・経度を正規化
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
表示結果は以下のようになります。
台風進路データには1951年から2016年までのデータが含まれていますが,csv.filter関数を使って2015年のデータのみを選択して可視化しています.
filtered= csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
ためしに上記箇所の2015を別の2000にしてみてください. 2000年の台風進路データが表示されます.
d3.js による可視化の醍醐味の一つとしてインタラクティブな可視化が実現できる点があります.
先程の台風進路データの可視化では, 可視化データ範囲を固定していましたが ユーザが操作できるようにします.
まず、「index.html」を編集し、テキストボックスとボタンを追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ひな形HTMLファイル</title>
<!-- d3.jsライブラリの読み込み -->
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v2.min.js"></script>
<!-- leaflet -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.js"></script>
<!-- スタイルシート -->
<link rel="stylesheet" href="style.css" type="text/css" />
<!-- 処理用スクリプト -->
<script type="text/javascript" src="script.js" ></script>
</head>
<body onload="init();">
<input id="year" type="number" min="1951" max="2016" step="1" />
<input type="button" value="更新" />
<div id="screen"></div>
</body>
</html>
つぎに「script.js」を以下のように変更します. 大きな変更点は新たに関数updateを定義していることです. 関数updateは引数に対応した台風進路データを可視化します.
// 台風進路データ格納用
var data;
var width = window.innerWidth;
var height = window.innerHeight;
// 座標変換用の関数定義
var prj = d3.geo.mercator()
.center([139.44,35.39])
.translate([width/2, height/2])
.scale(1000);
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
var color = d3.scale.category20();
/* onloadで呼び出される関数 */
function init(){
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// マッピング方法の設定
var path = d3.geo.path().projection(prj);
// topoJSONファイルの読み込み
var url = "JPN_adm0.topojson";
d3.json(url, function(error, json) {
// GeoJSON形式に変換
var geojson = topojson.feature(json, json.objects.JPN_adm0);
svg.selectAll('path')
.data(geojson.features).enter()
.append('path')
.attr('d', path)
.attr('fill','rgba(0,0,0,0.8)');
});
d3.csv("typhoon.csv",function(error,csv){
data = csv;
filterd = csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
svg.selectAll("circle")
.data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
// 更新用関数
function update(year){
var svg = d3.select("svg");
var circles = svg.selectAll("circle");
// 描画してあるcircleを全て削除する
circles.remove();
filterd = data.filter(function(row){
if(row.YEAR==year){
return true;
}
});
svg.selectAll("circle").data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
}
それではウェブブラウザで動作を確認してみましょう.
インプットボックスに可視化したい西暦を入力し,更新ボタンをクリックすると可視化するイメージです.
それでは,更新ボタンをクリックすると,関数updateを呼び出すようにindex.htmlを変更してみましょう.
index.html の更新ボタンを定義している下記箇所を
<input type="button" value="更新"/>
以下のように変更します.ボタンがクリックされると関数updateが呼び出されるようにします.
<input type="button" value="更新" onClick="update(d3.select('#year').property('value'));"/>
d3.js を使わず記述する場合は以下のようになります.更新ボタンがクリックされると,id「year」の要素が持つ属性「value」を取得し,関数「update」に渡します.
<input type="button" value="更新" onClick="update(document.getElementById('year').value);"/>
変更できたら動作を確認してみましょう.
d3.js の transition 関数を使うと簡易なアニメーションを実現できます. transition 関数は現時点の値から指定した値までの間を補完する関数となります. 実際に試してみましょう.script.js を以下のように変更してください.
var data;
var width = window.innerWidth;
var height = window.innerHeight;
// 座標変換用の関数定義
var prj = d3.geo.mercator()
.center([139.44,35.39])
.translate([width/2, height/2])
.scale(1000);
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
var color = d3.scale.category20();
/* onloadで呼び出される関数 */
function init(){
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// マッピング方法の設定
var path = d3.geo.path().projection(prj);
// topoJSONファイルの読み込み
var url = "JPN_adm0.topojson";
d3.json(url, function(error, json) {
// GeoJSON形式に変換
var geojson = topojson.feature(json, json.objects.JPN_adm0);
svg.selectAll('path')
.data(geojson.features).enter()
.append('path')
.attr('d', path)
.attr('fill','rgba(0,0,0,0.8)');
});
d3.csv("typhoon.csv",function(error,csv){
data = csv;
filterd = csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
svg.selectAll("circle")
.data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
// 更新用関数
function update(year){
var svg = d3.select("svg");
var circles = svg.selectAll("circle");
//500ミリ秒かけて半径を0にする。そして削除する。
circles.transition().delay(0).duration(500).attr("r",0).remove();
filterd = data.filter(function(row){
if(row.YEAR==year){
return true;
}
});
svg.selectAll("circle").data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.NAME;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",0);
// 読み込んで500ミリ秒後、1秒(1000ミリ秒)かけて半径を0からもとのサイズに変更します。
svg.selectAll("circle").transition().delay(500).duration(1000)
.attr("r",function(d){return grade(d.GRADE);});
}
更新ボタンをクリックして台風進路データの可視化がどのようにアニメーションされるかを確認してください.
d3.js のタイマ処理を利用するとより細かなアニメーションを実現できます. また,一定時間ごとにデータを取得するといった処理も実現できます.
script.jsを以下のように変更してください. 台風進路の描画をタイマ処理を用いて一行毎に可視化を行います.
var data = {};
var width = window.innerWidth;
var height = window.innerHeight;
// 座標変換用の関数定義
var prj = d3.geo.mercator()
.center([139.44,35.39])
.translate([width/2, height/2])
.scale(1000);
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
var color = d3.scale.category20();
/* onloadで呼び出される関数 */
function init(){
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// マッピング方法の設定
var path = d3.geo.path().projection(prj);
// topoJSONファイルの読み込み
var url = "JPN_adm0.topojson";
d3.json(url, function(error, json) {
// GeoJSON形式に変換
var geojson = topojson.feature(json, json.objects.JPN_adm0);
svg.selectAll('path')
.data(geojson.features).enter()
.append('path')
.attr('d', path)
.attr('fill','rgba(0,0,0,0.8)');
});
d3.csv("typhoon.csv",function(error,csv){
data = csv;
filterd = csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
svg.selectAll("circle")
.data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.ID;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
// 更新用関数
function update(year){
var svg = d3.select("svg");
var circles = svg.selectAll("circle");
var interval = 10;
circles.remove();
filterd = data.filter(function(row){
if(row.YEAR==year){
return true;
}
});
// 行インデックス
index = 0;
// アニメーション用関数
function annimation(){
// データを1つずつ読み込む
var d = filterd[index];
d3.selectAll("svg").append("circle")
.attr("id","id"+d.ID)
.attr("cx",prj([d.LON,d.LAT])[0])
.attr("cy",prj([d.LON,d.LAT])[1])
.attr("r",grade(d.GRADE))
.attr("fill",color(d.ID));
// 次の行に進む
index++;
if(filterd.length > index){
// データの最後でなければ、タイマーを仕込む
d3.timer(annimation,interval);
}
return true;
};
// 初回のタイマーを設定する
d3.timer(annimation,interval);
}
タイマ処理と先程のtransition関数を組み合わせると さらに凝ったアニメーションを実現できます.
script.js を以下のように書き換えてください.
var data = {};
var width = window.innerWidth;
var height = window.innerHeight;
// 座標変換用の関数定義
var prj = d3.geo.mercator()
.center([139.44,35.39])
.translate([width/2, height/2])
.scale(1000);
var grade = d3.scale.linear().domain([1,10]).range([1,20]);
var color = d3.scale.category20();
/* onloadで呼び出される関数 */
function init(){
// svg要素の追加
var svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
// マッピング方法の設定
var path = d3.geo.path().projection(prj);
// topoJSONファイルの読み込み
var url = "JPN_adm0.topojson";
d3.json(url, function(error, json) {
// GeoJSON形式に変換
var geojson = topojson.feature(json, json.objects.JPN_adm0);
svg.selectAll('path')
.data(geojson.features).enter()
.append('path')
.attr('d', path)
.attr('fill','rgba(0,0,0,0.8)');
});
d3.csv("typhoon.csv",function(error,csv){
data = csv;
filterd = csv.filter(function(row){
if(row.YEAR==2015){
return true;
}
});
svg.selectAll("circle")
.data(filterd)
.enter()
.append("circle")
.attr("id",function(d){return d.ID;})
.attr("cx",function(d){return prj([d.LON,d.LAT])[0];})
.attr("cy",function(d){return prj([d.LON,d.LAT])[1];})
.attr("fill",function(d,i){return color(d.NAME);})
.attr("r",function(d){return grade(d.GRADE);});
});
}
// 更新用関数
function update(year){
var svg = d3.select("svg");
var circles = svg.selectAll("circle");
var interval = 10;
circles.remove();
filterd = data.filter(function(row){
if(row.YEAR==year){
return true;
}
});
index = 0;
function annimation(){
// データを1つずつ読み込む
var d = filterd[index];
// 台風を描画する。5秒後から1秒間かけてサイズを0にする。そして削除する。
d3.selectAll("svg").append("circle")
.attr("id","id"+d.ID)
.attr("cx",prj([d.LON,d.LAT])[0])
.attr("cy",prj([d.LON,d.LAT])[1])
.attr("r",grade(d.GRADE))
.attr("fill",color(d.ID))
.transition()
.delay(5000)
.duration(1000)
.attr("r",0)
.remove();
// 次に進む
index++;
if(filterd.length > index){
// データの最後でなければ、タイマーを仕込む
d3.timer(annimation,interval);
}
return true;
};
// 初回のタイマー
d3.timer(annimation,interval);
}
d3.js の transition関数は面白い関数なので色々と試してみてください. 使いようによってはとても面白い可視化を実現できると思います.
ウェブブラウザではセキュリティを維持するため, 読み込んでいるサイトのドメイン以外からのデータ読み込みを禁止しており,これをクロスドメイン問題といいます. (たとえば、localhostで公開しているサイトは、localhost以外のサイトからデータを持ってくることはできません.)
複数のウェブサービスを組合せたウェブアプリを構築する場合などは障害となってくる問題です.
この問題を解決する手法はいくつかありますが、ここでは一番手軽な方法を紹介します.
クロスドメイン問題の例題として東京電力の電力使用状況の可視化をおこないます.
それでは東京電力の電力使用状況CSVを取得し,d3.jsで可視化します. script.js は以下のとおりです.
function init(){
var url = "http://www.tepco.co.jp/forecast/html/images/juyo-j.csv";
var width =window.innerWidth;
var height = window.innerHeight;
var svg = d3.select("#screen")
.append("svg")
.attr("width", width)
.attr("height", height);
d3.text(url, function(text) {
// テキストを改行で区切り、配列に格納する
var lines = text.split("\n");
// 0から8行目を削除する
lines.splice(0,8);
// 24行目以降を削除する
lines.splice(24);
// 配列を改行で区切ったテキストにする
text = lines.join("\n");
console.log(text);
// テキストを1行ずつ処理し、オブジェクトに変換する
var data = d3.csv.parseRows(text,function(row,index){
return {id: index,
date: row[0],
time: row[1],
pow: +row[2]};
});
var yScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.pow;})]).range([0,height]);
var xScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.id;})]).range([0,width]);
svg.selectAll("rect").data(data).enter().append("rect")
.attr("x",function(d){return xScale(d.id);})
.attr("y",function(d){return height-yScale(d.pow);})
.attr("width",width/data.length)
.attr("height",function(d){return (yScale(d.pow));})
// マウスカーソルがオブジェクト上にある場合の処理を定義する
.on("mouseover", function(d){
d3.select(this).attr("style", "fill:rgb(0,0,255)");
d3.select("#lbl").text(d.date+","+d.time+","+d.pow+"万kW");
})
// マウスカーソルがオブジェクト外にでたときの処理を定義する
.on("mouseout", function(d){d3.select(this).attr("style", "fill:rgb(0,0,0)");
d3.select("#lbl").text("");
});
});
}
index.html を以下のように変更します.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ひな形HTMLファイル</title>
<!-- d3.jsライブラリの読み込み -->
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v2.min.js"></script>
<!-- leaflet -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.js"></script>
<!-- スタイルシート -->
<link rel="stylesheet" href="style.css" type="text/css" />
<!-- 処理用スクリプト -->
<script type="text/javascript" src="script.js" ></script>
</head>
<body onload="init();">
<h1>東京電力:本日の電力消費量</h1>
<div id="screen"></div>
<label id="lbl"></label>
</body>
</html>
ブラウザで動作を確認してみてください. 本来は棒グラフが表示されるはずなのですが何も表示されていないと思います.
デベロッパーツールを開きコンソールを確認してください. アクセスが許可されていない旨のエラーメッセージを確認することができると思います.
クロスドメイン問題により, 東京電力のCSVデータを取得できていません.
今回は簡易的なプロキシサーバを用意し, このクロスドメイン問題に対応することにします. Javascriptからあたかも同ドメインのデータを取得しているかのように見せます.
script.jsを以下のように変更してください.
function init(){
var url = "http://www.tepco.co.jp/forecast/html/images/juyo-j.csv";
// 直接読み込むのではなく、代理読み込みする
var url = "cgi-bin/proxy.py?url="+url;
var width =window.innerWidth;
var height = window.innerHeight;
var svg = d3.select("#screen")
.append("svg")
.attr("width", width)
.attr("height", height);
d3.text(url, function(text) {
// テキストを改行で区切り、配列に格納する
var lines = text.split("\n");
// 0から8行目を削除する
lines.splice(0,8);
// 24行目以降を削除する
lines.splice(24);
// 配列を改行で区切ったテキストにする
text = lines.join("\n");
console.log(text);
// テキストを1行ずつ処理し、オブジェクトに変換する
var data = d3.csv.parseRows(text,function(row,index){
return {id: index,
date: row[0],
time: row[1],
pow: +row[2]};
});
var yScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.pow;})]).range([0,height]);
var xScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.id;})]).range([0,width]);
svg.selectAll("rect").data(data).enter().append("rect")
.attr("x",function(d){return xScale(d.id);})
.attr("y",function(d){return height-yScale(d.pow);})
.attr("width",width/data.length)
.attr("height",function(d){return (yScale(d.pow));})
// マウスカーソルがオブジェクト上にある場合の処理を定義する
.on("mouseover", function(d){
d3.select(this).attr("style", "fill:rgb(0,0,255)");
d3.select("#lbl").text(d.date+","+d.time+","+d.pow+"万kW");
})
// マウスカーソルがオブジェクト外にでたときの処理を定義する
.on("mouseout", function(d){d3.select(this).attr("style", "fill:rgb(0,0,0)");
d3.select("#lbl").text("");
});
});
}
変更は以下行の追加です.
var url = "cgi-bin/proxy.py?url="+url;
つぎに簡易プロキシサーバを用意します. ウェブルート配下にcgi-binというディレクトリを作り, そこに下記Pythonスクリプトを設置してください.
cgi-bin/proxy.py
proxy.py は以下のとおりとなります.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import cgi, urllib
import os
from logging import getLogger,StreamHandler,DEBUG
logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
form = cgi.FieldStorage()
try:
url = form['url'].value
#logger.debug("url:"+url)
#CGIHTTPServer固有の問題
if url.startswith('http://') is False:
url = url.replace("http:/",'http://')
if url.startswith('https://') is False:
url = url.replace('https:/','https://')
#ここまで
content = urllib.urlopen(url)
print 'Access-Control-Allow-Origin: *\n'
for line in content:
print line,
except:
print 'Content-Type: text/plain\n'
print 'error!'
※Linux, MacOSを使っている人はproxy.pyに下記コマンドで実行権限を与えてください.
chmod u+x proxy.py
一度ウェブサーバを停止し,ウェブルートで下記コマンドを実行してください(cgi-bin配下のプログラムを動かすために必要です).
python -m CGIHTTPServer
Python 3.x系の場合は以下のコマンドでCGIを有効にしてウェブサーバを起動します.
python -m http.server --cgi
それではウェブブラウザで起動したサーバ(http://localhost:8000)にアクセスしてください.
今度はちゃんと表示されたと思います.
クロスドメイン問題をより詳しく知りたい方は以下サイトを参考にしてください.
これまで d3.js を使ってCSVファイル,シェープファイルの可視化を試みてきました. d3.jsを使った可視化の締めくくりとしてXMLデータの可視化を試みます.
対象とするXMLデータは GeoRSS形式とします. GeoRSSはRSSにGIS情報を載せられるようにした形式です.
可視化に用いるGeoRSSデータは写真共有サイトのFlickerから取得します.
JSON形式で取得することもできるのですが, ここはあえてXML形式で取得します.
それでは早速可視化してみましょう.
index.htmlを以下の内容で書き換えてください.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ひな形HTMLファイル</title>
<!-- d3.jsライブラリの読み込み -->
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v2.min.js"></script>
<!-- leaflet -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet/v1.0.2/leaflet.js"></script>
<!-- スタイルシート -->
<link rel="stylesheet" href="style.css" type="text/css" />
<!-- 処理用スクリプト -->
<script type="text/javascript" src="script.js" ></script>
</head>
<body onload="init();">
<input type="button" value="grub" onClick="update();"/>
<div id="screen"></div>
<label id="lbl"></label>
</body>
</html>
script.js を下記の内容で書き換えてください. 基本的にはこれまでと同じですが, XMLデータからの値取得にXPathを使っています.
var width = window.innerWidth;
var height = window.innerHeight;
var img_width = 60;
var img_height = 45;
var url = "cgi-bin/proxy.py?url="+encodeURIComponent("https://www.flickr.com/services/feeds/geo");
var svg, prj;
/* onloadで呼び出される関数 */
function init(){
svg = d3.select("#screen")
.append("svg")
.attr("id","svg")
.attr("width", width)
.attr("height", height);
prj = d3.geo.mercator()
.center([0,0])
.translate([width/2, height/2])
.scale(width/2/Math.PI);
var path = d3.geo.path()
.projection(prj);
// https://raw.githubusercontent.com/topojson/topojson/1.6.19/examples/world-50m.json
d3.json("world-50m.json", function(error, world) {
if (error) throw error;
var color = d3.scale.category20();
var geojson = topojson.feature(world,world.objects.countries);
svg.selectAll("path").data(geojson.features)
.enter()
.append("path")
.attr("class", "land")
.attr("d", path)
.attr("id",function(d){return d.id;})
.attr("fill",function(d,i){return color(d.id);});
});
}
var update = function(){
// XMLを読み込みます
d3.xml(url, "application/xml", function(xml) {
var images = svg.selectAll("image")
// entry要素を読み込みます
.data(xml.documentElement.getElementsByTagName("entry"))
.enter()
.append("image")
.attr("xlink:href",function(d){
// link要素の中で属性rel=enclosureとなっているものを選択し、その属性hrefの値を取得する
return d.querySelector("link[rel=enclosure]").getAttribute("href");})
.attr("width",img_width+"px")
.attr("height",img_height+"px")
.attr("x",function(d){
// entry要素の子要素としてgeo:long要素のテキストを取得する。名前空間が付いているので、それを指定している。
var lon = d.getElementsByTagNameNS("http://www.w3.org/2003/01/geo/wgs84_pos#","long")[0].textContent;
var lat = d.getElementsByTagNameNS("http://www.w3.org/2003/01/geo/wgs84_pos#","lat")[0].textContent;
return prj([lon,lat])[0];})
.attr("y",function(d){
var lon = d.getElementsByTagNameNS("http://www.w3.org/2003/01/geo/wgs84_pos#","long")[0].textContent;
var lat = d.getElementsByTagNameNS("http://www.w3.org/2003/01/geo/wgs84_pos#","lat")[0].textContent;
return prj([lon,lat])[1];})
.on("click",function(d){
window.open(d.querySelector("link[rel=alternate]").getAttribute("href"));});
});
};
TopoJSONファイルは既存のものを利用します.下記サイトからダウンロードしてください.
https://raw.githubusercontent.com/topojson/topojson/1.6.19/examples/world-50m.json
今回もCGIを使うので「python -m CGIHTTPServer」を実行し, ウェブサーバにアクセスします.
「grub」ボタンをクリックすると,Flikerにアップロードされた最新の写真が地図上に表示されます.
また,画像をクリックすると当該写真のFlickerサイトにアクセスするようになっています.
下記課題に取り組み可視化に用いたファイル一式および可視化結果のスクリーンキャプチャを提出してください.ファイルはZIPでまとめてください.
「アニメーション+タイマ処理」のコードを改造し, 台風の気圧に応じて塗り色を変更してください(気圧が低ければ〜色,高ければ〜色のようなイメージ).
例:
d3.jsを使って自由にデータを可視化してください.これまでに内容を参考にしても,しなくても構いません.演習と同じ手法でデータだけが異なるといったものは認めません.
これまでブラウザを使ったオープンデータの可視化をガリガリとやってきましたが、 じつは、Rから直接HTMLファイルを出力するライブラリもあります。
rChartやShinyあたりがメジャーです。
rChart: http://rcharts.io/
rChartはライブラリ名のとおり、Rのグラフ描画をそのままHTMLファイル(Javascriptも使う)として出力するライブラリです。 (利用するにはコンパイル環境が必要となります。教育棟端末ではできない)
Shiny: http://shiny.rstudio.com/
ShinyはRをウェブアプリとして動かすためのライブラリです。(教育棟端末でも試せます)
処理負荷は高いですが、Rスクリプトをそのままサーバサイドで動かせるのが魅力です。
インストールは以下のとおりです。
install.packages("shiny")
Shinyは画面用のRスクリプトとサーバサイドのRスクリプトの2つが必要となります。
両方共同じ作業ディレクトリに設置します。
ui.R
library(shiny)
shinyUI(fluidPage(
titlePanel("Shiny: Sine Wave"),
sidebarLayout(
sidebarPanel(
numericInput("phs","Phase:",min=0,max=5,step = 0.1,value=0),
sliderInput("freq",
"Frequency(Hz):",
min = 1,
max = 20,
value = 1),
sliderInput("samp",
"Sampling Rates(Hz):",
min = 1,
max = 200,
value = 1)
),
mainPanel(
plotOutput("distPlot")
)
)
))
server.R
library(shiny)
library(ggplot2)
shinyServer(function(input, output) {
output$distPlot <- renderPlot({
t <- seq(from = 0, to = 1,by=1/input$samp)
data <- sin(2 * pi * input$freq * t + input$phs)
wave <- data.frame(time=t,wave=data)
ggplot(wave,aes(y=wave,x=time))+geom_bar(stat="identity")
})
})
ファイルを作成したら、コンソールに以下コマンドを入力し、サーバを起動します。
shiny::runApp()
実行例は以下のとおりです。スライダーを動かすと、正弦波のパラメータを変更できます。
ここまでRでする?というライブラリですが、個人的には面白い試みと思います。
Masaharu Hayashi を著作者とするこの 作品 は クリエイティブ・コモンズの 表示 4.0 国際 ライセンスで提供されています。